" Disassemble plgin for Vim
" Last Change: 2021-03-18
" Author: Kong Jun <kongjun18@outlook.com>
" Github: https://github.com/kongjun18
" License: GPL-2.0
"
" Note:
" 1. Only test GNU toolchain on GNU/Linux(Fedora 33)
" 2. Only support whole file disassemble
"
" TODO:
" 1. support visual mode selection
" 2. support float window???
" 3. use asynchronous API
" 4. compile source code only when buffer is modified
" 5. avoid file conflict
" 6. If executable path is invalid, try to compile current file
"
" Configuration:
" |---------------------------+-----------------------------------------------------------------|
" | variable                  | meaning                                                         |
" |---------------------------+-----------------------------------------------------------------|
" | disassemble_compiler      | compiler command                                                |
" | disassemble_objdump       | objdump command                                                 |
" | disassemble_compile_flags  | flags used to compile source code                               |
" | disassemble_objdump_flags | flags used to disassemble executable                            |
" | disassemble_source        | whether or not to generate assembly file from compiler directly |
" | disassemble_directory     | place all itermidiate files in it                               |
" | disassemble_executable    | disassemble this executable directly                            |
" | disassemble_loaded        | whether or not to load this plugin                              |
" | disassemble_no_command    | whether or not to create command :Disassemble                   |
" |---------------------------+-----------------------------------------------------------------|

" guard
if exists('g:disassemble_loaded') || &cp || v:version < 800
    finish
endif
let g:disassemble_loaded = 1

" wrapper of get()
function s:get(variable, default)
    if type(a:default) == v:t_list
        call assert_equal(type(a:variable), type(a:default))
        let l:var = get(b:, a:variable, get(g:, a:variable, []))
        return extend(l:var, a:default)
    else
        return get(b:, a:variable, get(g:, a:variable, a:default))
    endif
endfunction


" throw exception to exit program
function s:die(msg)
    throw 'vim-assemble: ' .. a:msg
endfunction

" print error message
function s:errmsg(msg)
    try
        echohl ErrorMsg
        echomsg a:msg
    finally
        echohl None
    endtry
endfunction
" wrapper of :execute, die when encounter error
function s:execute(...)
    if empty(a:000)
        return
    endif
    let l:command = join(a:000)
    silent exec '!'l:command
    call add(g:disassemble_commands, l:command)
    if v:shell_error
        call s:die('failed to execute ' .. l:command)
    endif
endfunction

function g:disassemble#disassemble() abort
    let l:source_file = expand('%:p')
    if empty(l:source_file)
        echoerr "Can't disassemble empty buffer"
        return
    endif

    " get toolchain and flags
    let l:compiler = ''
    let l:objdump = s:get('disassemble_objdump', 'objdump')
    let l:objdump_flags = join(s:get('disassemble_objdump_flags', ['-C', '-S', '-M sufix'])) " disassemble & demangle & show source code & keep suffix
    let l:compile_flags = join(s:get('disassemble_compile_flags', ['-S'])) " disassemble current file
    if &filetype == 'c'
        let l:compiler = s:get('disassemble_compiler', 'gcc')
    elseif &filetype == 'cpp'
        let l:compiler = s:get('disassemble_compiler', 'g++')
    else
        echoerr 'vim-disassemble only supports C/C++'
    endif

    let l:assembly_file = s:get('disassemble_directory', '/tmp') .. '/' .. substitute(expand('%:t'), '\..*$', '\.dis', '') "/path/to/buffer.extention --> /path/to/buffer.dis
    let l:executable = s:get('disassemble_executable', '')

    let g:disassemble_commands = []
    " disassemble executable directly
    try
        if !empty(l:executable)
            call s:execute(l:objdump, l:objdump_flags, l:executable, '>', l:assembly_file)
        else
            " use assembly file generated by compiler
            if (s:get('disassemble_source', v:true))
                let l:assembly_file = s:get('disassemble_directory', '/tmp') .. '/' .. substitute(expand('%:t'), '\..*$', '\.s', '')
                call s:execute(l:compiler, l:compile_flags, l:source_file, '-o', l:assembly_file)
            " compile and disassemble
            else
                let l:executable = s:get('disassemble_directory', '/tmp') .. '/' .. substitute(expand('%:t'), '\..*$', '\.out', '')
                let l:compile_flags = join(s:get('disassemble_compile_flags', ['']))
                call s:execute(l:compiler, l:compile_flags, l:source_file,'-o', l:executable)
                call s:execute(l:objdump, l:objdump_flags, l:executable, '>', l:assembly_file)
            endif
        endif
    catch /.*/
        call s:errmsg(v:exception)
        return
    endtry

    " buffer is no existed or hiddend
    if !bufexists(l:assembly_file) || bufexists(l:assembly_file) && empty(getbufinfo(l:assembly_file)[0].windows)
        execute 'vsp ' .. l:assembly_file
        " when we disassemble again, the buffer will change automatically
        call setbufvar(l:assembly_file, '&autoread', 1)
        let src_window_id = win_getid(winnr('#'))
        call win_gotoid(src_window_id)
    else
        if !bufloaded(l:assembly_file)
            call bufload(l:assembly_file)
        endif
    endif
endfunction

